🎖️GitЯра🎖️
Commit b4cc36043d5e545cff58fbe0a388e60c1a4ad14d
Parents : e1a5b3c
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-12T19:10:10Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-12T19:10:10Z
fix(ui): surface watchdog-forced reconnects in the nav connection icon (#6655)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Changes
2 files changed, 27 insertions(+), 4 deletions(-)
Diff
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
index ff02e7264c..d3c5a843c3 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt
@@ -63,6 +63,7 @@ import org.meshtastic.core.resources.connecting
import org.meshtastic.core.resources.device_sleeping
import org.meshtastic.core.resources.disconnected
import org.meshtastic.core.resources.node_restarting
+import org.meshtastic.core.resources.reconnecting
import org.meshtastic.core.ui.navigation.icon
import org.meshtastic.core.ui.viewmodel.UIViewModel
@@ -82,6 +83,7 @@ fun MeshtasticNavigationSuite(
) {
val connectionState by uiViewModel.connectionState.collectAsStateWithLifecycle()
val nodeRestartExpected by uiViewModel.nodeRestartExpected.collectAsStateWithLifecycle()
+ val watchdogReconnectInFlight by uiViewModel.watchdogReconnectInFlight.collectAsStateWithLifecycle()
val unreadMessageCount by uiViewModel.unreadMessageCount.collectAsStateWithLifecycle()
val selectedDevice by uiViewModel.currentDeviceAddressFlow.collectAsStateWithLifecycle()
@@ -108,6 +110,7 @@ fun MeshtasticNavigationSuite(
isSelected = isSelected,
connectionState = connectionState,
nodeRestartExpected = nodeRestartExpected,
+ watchdogReconnectInFlight = watchdogReconnectInFlight,
unreadMessageCount = unreadMessageCount,
selectedDevice = selectedDevice,
meshActivityFlow = uiViewModel.meshActivity,
@@ -185,13 +188,16 @@ private fun NavigationIconContent(
selectedDevice: String?,
meshActivityFlow: Flow<MeshActivity>,
nodeRestartExpected: Boolean = false,
+ watchdogReconnectInFlight: Boolean = false,
) {
val isConnectionsRoute = destination == TopLevelDestination.Connect
// An expected node restart (reboot-applying config save) presents as an in-progress state, not a scary
- // red disconnect: the icon borrows the Connecting treatment and the tooltip says "Restarting".
+ // red disconnect: the icon borrows the Connecting treatment and the tooltip says "Restarting". A
+ // watchdog-forced handshake recovery gets the same treatment with a "Reconnecting…" tooltip.
val restarting = nodeRestartExpected && connectionState != ConnectionState.Connected
+ val reconnecting = watchdogReconnectInFlight && !restarting
val presentedState =
- if (restarting && connectionState == ConnectionState.Disconnected) {
+ if ((restarting || reconnecting) && connectionState == ConnectionState.Disconnected) {
ConnectionState.Connecting
} else {
connectionState
@@ -203,7 +209,7 @@ private fun NavigationIconContent(
PlainTooltip {
Text(
if (isConnectionsRoute) {
- connectionTooltipLabel(restarting, connectionState)
+ connectionTooltipLabel(restarting, reconnecting, connectionState)
} else {
stringResource(destination.label)
},
@@ -249,8 +255,13 @@ private fun NavigationIconContent(
}
@Composable
-private fun connectionTooltipLabel(restarting: Boolean, connectionState: ConnectionState): String = when {
+private fun connectionTooltipLabel(
+ restarting: Boolean,
+ reconnecting: Boolean,
+ connectionState: ConnectionState,
+): String = when {
restarting -> stringResource(Res.string.node_restarting)
+ reconnecting -> stringResource(Res.string.reconnecting)
connectionState == ConnectionState.Connected -> stringResource(Res.string.connected)
connectionState == ConnectionState.Connecting -> stringResource(Res.string.connecting)
connectionState == ConnectionState.DeviceSleep -> stringResource(Res.string.device_sleeping)
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
index 95432feec0..b8de08558e 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
@@ -108,6 +108,18 @@ class UIViewModel(
/** True while the connected node is expected to be mid-restart (reboot-applying config save or reboot command). */
val nodeRestartExpected: StateFlow<Boolean> = nodeRestartTracker.restartExpected
+ /**
+ * True while the handshake-stall watchdog is force-reconnecting the transport, so the UI can present the transient
+ * Disconnected window as an in-progress recovery rather than a user-visible disconnect. Same signal contract as
+ * [ConnectionsViewModel.connectionStatus]'s RECONNECTING case.
+ */
+ val watchdogReconnectInFlight: StateFlow<Boolean> =
+ combine(serviceRepository.connectionState, serviceRepository.connectionProgress) { state, progress ->
+ state is ConnectionState.Disconnected && progress == ServiceRepository.RECONNECTING_PROGRESS_TEXT
+ }
+ .distinctUntilChanged()
+ .stateInWhileSubscribed(initialValue = false)
+
private val _navigationDeepLink = MutableSharedFlow<List<NavKey>>(replay = 1)
val navigationDeepLink = _navigationDeepLink.asSharedFlow()
Served by rngit 1.5.2 - Generated in 0.05s